/* 
 * Copyright (C)2008 Vladimir Hozjan vladimir.hozjan@gmail.com
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
 */

/**
 * @file  tetris.cpp
 *
 * Tetris engine implementation
 *
 * @author Vladimir Hozjan
 * @date   25.12.2008 
 *
 * @remarks VHOZ [25.12.2008]: Class created
 */
#include "tetris.h"
#include "tetrisBackground.h"
#include "tetrisElement.h"

//////////////////////////////////////////////////////////////  GLOBAL VARIABLES
static const unsigned short gShape01_2[] = {1,1};
static const unsigned short gShape01_1[] = {1,1};
static const unsigned short *gShape01[] = {gShape01_1, gShape01_2};
static const unsigned short gShape02_4[] = {0,0,0,0};
static const unsigned short gShape02_3[] = {0,0,0,0};
static const unsigned short gShape02_2[] = {1,1,1,1};
static const unsigned short gShape02_1[] = {0,0,0,0};
static const unsigned short *gShape02[] = {gShape02_1, gShape02_2, gShape02_3, gShape02_4};
static const unsigned short gShape03_3[] = {0,0,0};
static const unsigned short gShape03_2[] = {0,1,1};
static const unsigned short gShape03_1[] = {1,1,0};
static const unsigned short *gShape03[] = {gShape03_1, gShape03_2, gShape03_3};
static const unsigned short gShape04_3[] = {0,0,0};
static const unsigned short gShape04_2[] = {1,1,0};
static const unsigned short gShape04_1[] = {0,1,1};
static const unsigned short *gShape04[] = {gShape04_1, gShape04_2, gShape04_3};
static const unsigned short gShape05_3[] = {0,1,0};
static const unsigned short gShape05_2[] = {0,1,0};
static const unsigned short gShape05_1[] = {1,1,0};
static const unsigned short *gShape05[] = {gShape05_1, gShape05_2, gShape05_3};
static const unsigned short gShape06_3[] = {0,1,0};
static const unsigned short gShape06_2[] = {0,1,0};
static const unsigned short gShape06_1[] = {0,1,1};
static const unsigned short *gShape06[] = {gShape06_1, gShape06_2, gShape06_3};
static const unsigned short gShape07_3[] = {0,0,0};
static const unsigned short gShape07_2[] = {1,1,1};
static const unsigned short gShape07_1[] = {0,1,0};
static const unsigned short *gShape07[] = {gShape07_1, gShape07_2, gShape07_3};

//////////////////////////////////////////////////////////  CLASS IMPLEMENTATION
/**
 * Tetris
 *
 * @return 
 */
Tetris::Tetris(SystemInterface & aSystemInterface, unsigned short aNumRow, unsigned short aNumColumn) :
mPositionChangeMethod(0),
mElementChangeMethod(0),
mSystem(aSystemInterface),
mLevel(0),
mNumLines(0),
mScore(0),
mGameOver(false),
mError(false),
mNumRows(aNumRow),
mNumCols(aNumColumn),
mRow(aNumRow-1),
mColumn(aNumColumn/2),
mCurrElement(0),
mBackground(0)
{
  /** Create elements in element list.*/
  mElementList = new TetrisElement*[MAX_ELEMENTS];
  mElementList[0] = new TetrisElement(1, 2, gShape01);
  mElementList[1] = new TetrisElement(2, 4, gShape02);
  mElementList[2] = new TetrisElement(3, 3, gShape03);
  mElementList[3] = new TetrisElement(4, 3, gShape04);
  mElementList[4] = new TetrisElement(5, 3, gShape05);
  mElementList[5] = new TetrisElement(6, 3, gShape06);
  mElementList[6] = new TetrisElement(7, 3, gShape07);

  /** Create background.*/
  mBackground = new TetrisBackground(mNumCols, mNumRows + MAX_ELEMENT_DIMENSION);
  
  /* Create current and next element.*/
  mNextElement = new TetrisElement(*mElementList[mSystem.getRandom(MAX_ELEMENTS)]);
  mCurrElement = new TetrisElement(*mElementList[mSystem.getRandom(MAX_ELEMENTS)]);

  /** Check if successfully allocated elements.*/
  if (mBackground == 0 || mNextElement == 0 || mCurrElement == 0) {
    mError = true;
  }

  /** Set start position and next element.*/
  setStartPositionAndNextElement();
}

/**
 * ~Tetris
 *
 * Destructor
 */
Tetris::~Tetris() {
  /** Destroy all objects */
  if (mBackground) {
    delete mBackground;
  }
  if (mCurrElement) {
    delete mCurrElement;
  }
  if (mNextElement) {
    delete mNextElement;
  }
  if (mPositionChangeMethod) {
    delete mPositionChangeMethod;
  }
  if (mElementChangeMethod) {
    delete mElementChangeMethod;
  }
  int i;
  for (i = 0; i < MAX_ELEMENTS; i++) {
    if (mElementList[i] != 0) {
      delete mElementList[i];
    }
  }
  delete [] mElementList;
}

/**
 * setPositionChangeCallback
 *
 * @param const ITetrisFunctor * aChangeMethod
 * @return void
 */
void Tetris::setPositionChangeCallback( ITetrisFunctor & aChangeMethod ) {
  if (mPositionChangeMethod != 0) {
    delete mPositionChangeMethod;
  }
  mPositionChangeMethod = aChangeMethod.duplicate();
}

/**
 * setElementChangeCallback
 *
 * @param const ITetrisFunctor * aChangeMethod
 * @return void
 */
void Tetris::setElementChangeCallback( ITetrisFunctor & aChangeMethod ) {
  if (mElementChangeMethod != 0) {
    delete mElementChangeMethod;
  }
  mElementChangeMethod = aChangeMethod.duplicate();
  notifyElementChange();
}

/**
 * moveLeft
 *
 * Move element left if possible.
 *
 * @return void
 */
void Tetris::moveLeft() {
  /** Check if it is space in the background to go left */
  if (!mError && !mGameOver) {
    if (mBackground->isSpaceForElement( mRow, mColumn-1, mCurrElement)) {
      mColumn--;
    }
    notifyPositionChange();
  }
}

/**
 * moveRight
 *
 * Move element right if possible
 *
 * @return void
 */
void Tetris::moveRight() {
  /** Check if it is space in the background to go right */
  if (!mError && !mGameOver) {
    if (mBackground->isSpaceForElement( mRow, mColumn+1, mCurrElement)) {
      mColumn++;
    }
    notifyPositionChange();
  }
}

/**
 * moveDown
 *
 * Move element down if possible.
 *
 * @return void
 */
void Tetris::moveDown() {
  if (!mError && !mGameOver) {
    /** Check if it is space in the background to go down */
    if (mBackground->isSpaceForElement( mRow - 1, mColumn, mCurrElement)) {
      /** put element lower */
      mRow--;
    } else {
      /*element is down*/
      elementDown();
    }
    notifyPositionChange();
  }
}

/**
 * drop
 *
 * Drop element. Recalculate level and score.
 *
 * @return void
 */
void Tetris::drop() {
  if (!mError && !mGameOver) {
    /** Check if it is space in the background to go down */
    while (mBackground->isSpaceForElement( mRow - 1, mColumn, mCurrElement)) {
      /** put element lower */
      mRow--;
    }
    /* Element is down.*/
    elementDown();
    notifyPositionChange();
  }
}

/**
 * RotateLeft
 *
 * Rotate element to left if possible.
 *
 * @return void
 */
void Tetris::rotateLeft() {
  if (!mError && !mGameOver) {
    /** Rotate temporary element and check if is space for element.*/
    TetrisElement *tmp = new TetrisElement(*mCurrElement);
    tmp->rotateElementLeft();
    if (mBackground->isSpaceForElement(mRow, mColumn, tmp)) {
      mCurrElement->rotateElementLeft();
    }
    delete tmp;
    notifyPositionChange();
  }
}

/**
 * rotateRight
 *
 * Rotate element to right if possible.
 *
 * @return void
 */
void Tetris::rotateRight() {
  if (!mError && !mGameOver) {
    /** Rotate temporary element and check if is space for element.*/
    TetrisElement *tmp = new TetrisElement(*mCurrElement);
    tmp->rotateElementRight();
    if (mBackground->isSpaceForElement(mRow, mColumn, tmp)) {
      mCurrElement->rotateElementRight();
    }
    delete tmp;
    notifyPositionChange();
  }
}

/**
 * nextMove
 *
 * Tick moves element for one row down. 
 * If element is on bottom check if is game over.
 * If element is on bottom recalculate level and score.
 * Notify about change.
 *
 * @return void
 */
void Tetris::nextMove() {
  moveDown();
}

/**
 * getDelay
 *
 * Calculate delay from level
 *
 */
unsigned short Tetris::getDelay() const {
  unsigned short delay = 0;
  switch (mLevel) {
    case  0:delay=800;  break;
    case  1:delay=650;  break;
    case  2:delay=500;  break;
    case  3:delay=400;  break;
    case  4:delay=350;  break;
    case  5:delay=300;  break;
    case  6:delay=250;  break;
    case  7:delay=200;  break;
    case  8:delay=150;  break;
    case  9:delay=120;  break;
    case 10:delay=100;	break;
    case 11:delay=90;	  break;
    case 12:delay=80;	  break;
    case 13:delay=70;	  break;
    case 14:delay=60;	  break;
    case 15:delay=50;	  break;
    case 16:delay=40;	  break;
    case 17:delay=30;	  break;
    case 18:delay=20;	  break;
    case 19:delay=18;	  break;
    case 20:delay=15;	  break;
    case 21:delay=10;	  break;
    case 22:delay=5;		break;
    case 23:delay=2;		break;
    case 24:delay=1;    break;
    default: delay = 0;
  }
  return delay;
}

/**
 * setStartPositionAndNextElement
 *
 * Set start position for current element.
 * Next element becomes current element. 
 * New next element is selected.
 *
 */
void Tetris::setStartPositionAndNextElement() {
  if (mCurrElement != 0) {
    delete mCurrElement;
  }
  mCurrElement = mNextElement;
  mNextElement = new TetrisElement(*mElementList[mSystem.getRandom(MAX_ELEMENTS)]);
  mRow = mNumRows-1;
  mColumn = (mNumCols - mCurrElement->getDimension())/2;
  if(!mBackground->isSpaceForElement(mRow, mColumn, mCurrElement)){
    mGameOver = true;
    /*Move element from background*/
    mRow = mNumRows;
  } else {
    notifyElementChange();
  }
}

/**
 * getCell
 *
 * return value of a cell in tetris game
 *
 * @param[in] aRow
 * @param[in] aCol
 *
 * @return Method return value between 0 and 8. If cell doesn't exists return 0.
 */
unsigned short Tetris::getCell( unsigned short aRow, unsigned short aCol ) const {
  unsigned short retval = 0;
  unsigned short size = mCurrElement->getDimension();
  /** Check valid input values */
  if (mBackground != 0 && mCurrElement != 0) {
    if ((aRow < mNumRows) && (aCol < mNumCols)) {
      retval = mBackground->getCell(aRow, aCol);
      /** If value different from 0, it can be assumed that no cell within element is in that location.
        * This is done because of optimization reason.
        */
      if (retval == 0) {  
        /** Check if element is over coordinates aRow, aColl*/
        if ((aRow >= mRow) && (aRow < mRow + size) && (aCol >= mColumn ) && aCol <= mColumn + size) {
          retval = mCurrElement->getCell(aRow-mRow, aCol - mColumn);
        }
      }
    }
  }
  return retval;
}

/**
 * getDimensionOfNextElement
 *
 * Returns dimension of next element.
 *
 * @return Dimension of the next element. If error return 0;
 */
unsigned short Tetris::getDimensionOfNextElement() const {
  unsigned short retval = 0;
  if ( mNextElement != 0) {
    retval = mNextElement->getDimension();
  }
  return retval;
}

/**
 * getCellOfNextElement
 *
 * Returns value of cells in next element.
 *
 * @param[in] aRow
 * @param[in] aCol
 *
 * @return Value of cell between 0 and 8
 *
 */
unsigned short Tetris::getCellOfNextElement(unsigned short aRow, unsigned short aCol) const {
  unsigned short retval = 0;
  /*Check pointer*/
  if (mNextElement != 0) {
    /*Check input parameters*/
    if (aRow < mNextElement->getDimension() && aCol < mNextElement->getDimension()) {
      retval = mNextElement->getCell(aRow, aCol);
    }
  }
  return retval;
}

/**
 * elementDown
 *
 * Element is down. Put element in background. Calculate full lines
 * and recalculate level and score.
 *
 */
void Tetris::elementDown() {
  /** Check how many lines are full and return that value*/
  mBackground->insertElement(mRow, mColumn, mCurrElement);
  unsigned short numFullLines = mBackground->countFullLines();
  if (numFullLines > 0) {
    mBackground->clearFullLines();
  }
  calculateScoreAndLevel(numFullLines, mCurrElement->geElementId());
  setStartPositionAndNextElement();
}

/**
 * calculateScoreAndLevel
 *
 * Calculates new score and level
 *
 * @param[in] unsigned short aElementId
 */
void Tetris::calculateScoreAndLevel( unsigned short aNumLines, unsigned short aElementId ) {
  long bonus;

  /* Bonus cares for randomness in score value */
  switch (aElementId) {
  case 0: bonus=2; break;
  case 1: bonus=2; break;
  case 2: bonus=8; break;
  case 3: bonus=8; break;
  case 4: bonus=4; break;
  case 5: bonus=4; break;
  case 6: bonus=5; break;
  default: bonus = 0;
  }

  /* Calculate number of lines */
  mNumLines += aNumLines;

  /* Recalculate level */
  if((mNumLines/10) > mLevel) {
    mLevel = (mNumLines / 10);
  }

  mScore += 4*int((50*(aNumLines*1.5)*mSystem.sqrt((double)(mLevel+1)))+bonus*mSystem.sqrt((double)(mLevel+1)));
  
}

/**
 * notifyPositionChange
 *
 * long_description
 *
 */
void Tetris::notifyPositionChange() {
  if (mPositionChangeMethod != 0) {
    (*mPositionChangeMethod)(); 
  }
}

/**
 * notifyElementChange
 *
 * Old element came down to the bottom and new element appeared. Score, level and lines are recalculated
 *
 */
void Tetris::notifyElementChange() {
  if (mElementChangeMethod != 0) {
    (*mElementChangeMethod)(); 
  }
}

